﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Pipra.Math;

namespace Pipra.Gis
{
    [Obsolete("This should be somewhere in Math and be based on Transformations and not CoordinateOperations.")]
    public class CoordinateOperationCompiled<TSource,TTarget> :
        ITransformation<TSource,TTarget>,
        ICoordinateOperationConcatenated
    {
        private static readonly Type GenericCoordOpDefn = typeof(ITransformation<,>);
        private const string TransformValueMethodName = "TransformValue";

        private static Type FindCommonType(ITransformation left, ITransformation right)
        {
            IEnumerable<Type> leftTypes = left.GetType()
                .GetInterfaces()
                .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == GenericCoordOpDefn)
                .Select(t => t.GetGenericArguments()[1])
            ;
            List<Type> rightTypes = right.GetType()
                .GetInterfaces()
                .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == GenericCoordOpDefn)
                .Select(t => t.GetGenericArguments()[0])
                .ToList()
            ;
            return leftTypes.First(rightTypes.Contains);
        }

        private static List<Type> CreateBridgeTypes(List<ITransformation> ops)
        {
            var bridgeTypes = new List<Type>(ops.Count + 1);
            bridgeTypes.Add(typeof(TSource));
            for (int i = 1; i < ops.Count; i++)
            {
                ITransformation leftOp = ops[i - 1];
                ITransformation rightOp = ops[i];
                bridgeTypes.Add(FindCommonType(leftOp, rightOp));
            }
            bridgeTypes.Add(typeof(TTarget));
            return bridgeTypes;
        }

        private static MethodInfo FindTransformMethod(Type opType, Type sourceType, Type targetType)
        {
            var opInterfaceType =
                    opType.GetInterfaces()
                        .First(t =>
                               t.IsGenericType
                               && t.GetGenericTypeDefinition() == GenericCoordOpDefn
                               && t.GetGenericArguments().Length == 2
                               && t.GetGenericArguments()[0] == sourceType
                               && t.GetGenericArguments()[1] == targetType
                        )
                    ;
            return opType.GetMethods().Concat(opInterfaceType.GetMethods())
                .First(m =>
                    m.Name == TransformValueMethodName
                    && m.GetParameters().Length == 1
                    && m.GetParameters()[0].ParameterType == sourceType
                    && m.ReturnType == targetType
                )
            ;
        }

        private static Expression BuildExpression(
            ParameterExpression param,
            IEnumerable<ITransformation> operations
        )
        {
            var ops = new List<ITransformation>(operations);
            var bridgeTypes = CreateBridgeTypes(ops);

            Expression exp = param;
            for(int i = 0; i < ops.Count; i++)
            {
                var op = ops[i];
                var method = FindTransformMethod(
                    op.GetType(),
                    bridgeTypes[i],
                    bridgeTypes[i + 1]
                );
                exp = Expression.Call(
                    Expression.Constant(op),
                    method,
                    exp
                );
            }
            return exp;

        }

        private static Func<TSource, TTarget> Compile(IEnumerable<ITransformation> operations)
        {
            ParameterExpression singleCoordParam = Expression.Parameter(typeof(TSource), "coord");
            return Expression.Lambda<Func<TSource, TTarget>>(BuildExpression(singleCoordParam, operations), singleCoordParam).Compile();
        }

        private readonly ITransformation[] _ops;
        private readonly Func<TSource, TTarget> _convertValue;

        /// <summary>
        /// Constructs a new compiled concatenated operation.
        /// </summary>
        /// <param name="operations">The ordered set of operations to concatenate.</param>
        public CoordinateOperationCompiled(IEnumerable<ITransformation> operations)
        {
            _ops = null == operations ? new ITransformation[0] : operations.ToArray();
            _convertValue = Compile(_ops);
        }

        public TTarget TransformValue(TSource value)
        {
            return _convertValue(value);
        }

        public IEnumerable<TTarget> TransformValues(IEnumerable<TSource> values)
        {
            return values.Select(_convertValue);
        }

        ITransformation<TTarget, TSource> ITransformation<TSource, TTarget>.GetInverse()
        {
            if (!HasInverse)
            {
                throw new InvalidOperationException("No inverse.");
            }
            return new CoordinateOperationCompiled<TTarget, TSource>(
                this
                    .Select(co => co.GetInverse())
                    .Reverse()
            );
        }

        public bool HasInverse
        {
            get { return _ops.All(co => co.HasInverse); }
        }

        ITransformation ITransformation.GetInverse()
        {
            return GetInverse() as ITransformation;
        }

        public IEnumerator<ITransformation> GetEnumerator()
        {
            return _ops.AsEnumerable().GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public ITransformation<TTarget, TSource> GetInverse()
        {
            if (!HasInverse)
            {
                throw new InvalidOperationException("No inverse.");
            }
            return new CoordinateOperationCompiled<TTarget, TSource>(
                this
                    .Select(co => co.GetInverse())
                    .Reverse()
            );
        }

    }
}
